import type {
    ColDef,
    ColKey,
    ColumnEventType,
    IColumnCollectionService,
    NamedBean,
    PropertyValueChangedEvent,
    _ColumnCollections,
} from 'ag-grid-community';
import {
    AgColumn,
    BeanStub,
    GROUP_AUTO_COLUMN_ID,
    _addColumnDefaultAndTypes,
    _applyColumnState,
    _areColIdsEqual,
    _columnsMatch,
    _convertColumnEventSourceType,
    _destroyColumnTree,
    _getColumnStateFromColDef,
    _isColumnsSortingCoupledToGroup,
    _isGroupMultiAutoColumn,
    _isGroupUseEntireRow,
    _mergeDeep,
    _missing,
    _updateColsMap,
    _warn,
    isColumnGroupAutoCol,
} from 'ag-grid-community';

export class AutoColService extends BeanStub implements NamedBean, IColumnCollectionService {
    beanName = 'autoColSvc' as const;

    /** Group auto columns */
    public columns: _ColumnCollections | null;

    public postConstruct(): void {
        this.addManagedPropertyListener('autoGroupColumnDef', this.updateColumns.bind(this));
    }

    public addColumns(cols: _ColumnCollections): void {
        if (this.columns == null) {
            return;
        }
        cols.list = this.columns.list.concat(cols.list);
        cols.tree = this.columns.tree.concat(cols.tree);
        _updateColsMap(cols);
    }

    public createColumns(
        cols: _ColumnCollections,
        updateOrders: (callback: (cols: AgColumn[] | null) => AgColumn[] | null) => void,
        source: ColumnEventType
    ): void {
        const beans = this.beans;
        const { colModel, gos, rowGroupColsSvc, colGroupSvc } = beans;
        const isPivotMode = colModel.isPivotMode();
        const groupFullWidthRow = _isGroupUseEntireRow(gos, isPivotMode);
        // we need to allow suppressing auto-column separately for group and pivot as the normal situation
        // is CSRM and user provides group column themselves for normal view, but when they go into pivot the
        // columns are generated by the grid so no opportunity for user to provide group column. so need a way
        // to suppress auto-col for grouping only, and not pivot.
        // however if using Viewport RM or SSRM and user is providing the columns, the user may wish full control
        // of the group column in this instance.
        const suppressAutoColumn = isPivotMode ? gos.get('pivotSuppressAutoColumn') : this.isSuppressAutoCol();

        const rowGroupCols = rowGroupColsSvc?.columns;

        const groupingActive = (rowGroupCols && rowGroupCols.length > 0) || gos.get('treeData');

        const noAutoCols = !groupingActive || suppressAutoColumn || groupFullWidthRow;

        const destroyPrevious = () => {
            if (this.columns) {
                _destroyColumnTree(beans, this.columns.tree);
                this.columns = null;
            }
        };

        // function
        if (noAutoCols) {
            destroyPrevious();
            return;
        }

        const list = this.generateAutoCols(rowGroupCols);
        const autoColsSame = _areColIdsEqual(list, this.columns?.list || null);

        // the new tree depth will equal the current tree depth of cols
        const newTreeDepth = cols.treeDepth;
        const oldTreeDepth = this.columns ? this.columns.treeDepth : -1;
        const treeDepthSame = oldTreeDepth == newTreeDepth;

        if (autoColsSame && treeDepthSame) {
            // Some things like header could have changed, ensure this is captured by updating the existing cols.
            const colsMap = new Map(list.map((col) => [col.getId(), col]));
            for (const col of this.columns?.list ?? []) {
                const newDef = colsMap.get(col.getId());
                if (newDef) {
                    col.setColDef(newDef.getColDef(), null, source);
                }
            }
            return;
        }

        destroyPrevious();
        const treeDepth = colGroupSvc?.findDepth(cols.tree) ?? 0;
        const tree = colGroupSvc?.balanceTreeForAutoCols(list, treeDepth) ?? [];
        this.columns = {
            list,
            tree,
            treeDepth,
            map: {},
        };

        const putAutoColsFirstInList = (cols: AgColumn[] | null): AgColumn[] | null => {
            if (!cols) {
                return null;
            }
            // we use colId, and not instance, to remove old autoGroupCols
            const colsFiltered = cols.filter((col) => !isColumnGroupAutoCol(col));
            return [...list, ...colsFiltered];
        };

        updateOrders(putAutoColsFirstInList);
    }

    public updateColumns(event: PropertyValueChangedEvent<'autoGroupColumnDef'>) {
        const source = _convertColumnEventSourceType(event.source);
        this.columns?.list.forEach((col, index) => this.updateOneAutoCol(col, index, source));
    }

    public getColumn(key: ColKey): AgColumn | null {
        return this.columns?.list.find((groupCol) => _columnsMatch(groupCol, key)) ?? null;
    }

    public getColumns(): AgColumn[] | null {
        return this.columns?.list ?? null;
    }

    private generateAutoCols(rowGroupCols: AgColumn[] = []): AgColumn[] {
        const autoCols: AgColumn[] = [];
        const { gos } = this;

        const doingTreeData = gos.get('treeData');
        let doingMultiAutoColumn = _isGroupMultiAutoColumn(gos);

        if (doingTreeData && doingMultiAutoColumn) {
            _warn(182);
            doingMultiAutoColumn = false;
        }

        // if doing groupDisplayType = "multipleColumns", then we call the method multiple times, once
        // for each column we are grouping by
        if (doingMultiAutoColumn) {
            rowGroupCols.forEach((rowGroupCol, index) => {
                autoCols.push(this.createOneAutoCol(rowGroupCol, index));
            });
        } else {
            autoCols.push(this.createOneAutoCol());
        }

        return autoCols;
    }

    private isSuppressAutoCol() {
        const gos = this.gos;
        const groupDisplayType = gos.get('groupDisplayType');
        const isCustomRowGroups = groupDisplayType === 'custom';
        if (isCustomRowGroups) {
            return true;
        }

        const treeDataDisplayType = gos.get('treeDataDisplayType');
        return treeDataDisplayType === 'custom';
    }

    // rowGroupCol and index are missing if groupDisplayType != "multipleColumns"
    private createOneAutoCol(rowGroupCol?: AgColumn, index?: number): AgColumn {
        // if doing multi, set the field
        let colId: string;
        if (rowGroupCol) {
            colId = `${GROUP_AUTO_COLUMN_ID}-${rowGroupCol.getId()}`;
        } else {
            colId = GROUP_AUTO_COLUMN_ID;
        }

        const colDef = this.createAutoColDef(colId, rowGroupCol, index);
        colDef.colId = colId;

        const newCol = new AgColumn(colDef, null, colId, true);
        this.createBean(newCol);
        return newCol;
    }

    /**
     * Refreshes an auto group col to load changes from defaultColDef or autoGroupColDef
     */
    private updateOneAutoCol(colToUpdate: AgColumn, index: number, source: ColumnEventType) {
        const oldColDef = colToUpdate.getColDef();
        const underlyingColId = typeof oldColDef.showRowGroup == 'string' ? oldColDef.showRowGroup : undefined;
        const beans = this.beans;
        const underlyingColumn = underlyingColId != null ? beans.colModel.getColDefCol(underlyingColId) : undefined;
        const colId = colToUpdate.getId();
        const colDef = this.createAutoColDef(colId, underlyingColumn ?? undefined, index);

        colToUpdate.setColDef(colDef, null, source);

        _applyColumnState(beans, { state: [_getColumnStateFromColDef(colDef, colId)] }, source);
    }

    private createAutoColDef(colId: string, underlyingColumn?: AgColumn, index?: number): ColDef {
        // if one provided by user, use it, otherwise create one
        let res: ColDef = this.createBaseColDef(underlyingColumn);

        const autoGroupColumnDef = this.gos.get('autoGroupColumnDef');
        _mergeDeep(res, autoGroupColumnDef);

        res = _addColumnDefaultAndTypes(this.beans, res, colId, true);

        // For tree data the filter is always allowed
        if (!this.gos.get('treeData')) {
            // we would only allow filter if the user has provided field or value getter. otherwise the filter
            // would not be able to work.
            const noFieldOrValueGetter =
                _missing(res.field) &&
                _missing(res.valueGetter) &&
                _missing(res.filterValueGetter) &&
                res.filter !== 'agGroupColumnFilter';
            if (noFieldOrValueGetter) {
                res.filter = false;
            }
        }

        // if showing many cols, we don't want to show more than one with a checkbox for selection
        if (index && index > 0) {
            res.headerCheckboxSelection = false;
        }

        const isSortingCoupled = _isColumnsSortingCoupledToGroup(this.gos);
        const hasOwnData = res.valueGetter || res.field != null;
        if (isSortingCoupled && !hasOwnData) {
            // if col is coupled sorting, and has sort attribute, we want to ignore this
            // because we only accept the sort on creation of the col
            res.sortIndex = undefined;
            res.initialSort = undefined;
        }

        return res;
    }

    private createBaseColDef(rowGroupCol?: AgColumn): ColDef {
        const userDef = this.gos.get('autoGroupColumnDef');
        const localeTextFunc = this.getLocaleTextFunc();

        const res: ColDef = {
            headerName: localeTextFunc('group', 'Group'),
            showRowGroup: rowGroupCol?.getColId() ?? true,
        };

        const userHasProvidedGroupCellRenderer = userDef && (userDef.cellRenderer || userDef.cellRendererSelector);

        // only add the default group cell renderer if user hasn't provided one
        if (!userHasProvidedGroupCellRenderer) {
            res.cellRenderer = 'agGroupCellRenderer';
        }

        if (rowGroupCol) {
            res.headerName = this.beans.colNames.getDisplayNameForColumn(rowGroupCol, 'header') ?? undefined;
            res.headerValueGetter = rowGroupCol.colDef.headerValueGetter;
        }

        return res;
    }

    public override destroy(): void {
        _destroyColumnTree(this.beans, this.columns?.tree);
        super.destroy();
    }
}
